//   Copyright 2012,2013 Vaughn Vernon
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.

package com.saasovation.collaboration.port.adapter.persistence.view;

import com.saasovation.collaboration.domain.model.calendar.*;
import com.saasovation.collaboration.domain.model.collaborator.Participant;
import com.saasovation.collaboration.domain.model.tenant.Tenant;
import com.saasovation.common.event.sourcing.DispatchableDomainEvent;
import com.saasovation.common.event.sourcing.EventDispatcher;
import com.saasovation.common.port.adapter.persistence.AbstractProjection;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;

import java.util.HashMap;
import java.util.Map;

public class MySQLCalendarEntryProjection extends AbstractProjection
        implements EventDispatcher {

    private final NamedParameterJdbcTemplate jdbcTemplate;

    private static final Class<?>[] understoodEventTypes = {
            CalendarEntryDescriptionChanged.class,
            CalendarEntryParticipantInvited.class,
            CalendarEntryParticipantUninvited.class,
            CalendarEntryRelocated.class,
            CalendarEntryRescheduled.class,
            CalendarEntryScheduled.class
    };

    public MySQLCalendarEntryProjection(EventDispatcher aParentEventDispatcher,
                                        NamedParameterJdbcTemplate jdbcTemplate) {
        super();
        this.jdbcTemplate = jdbcTemplate;
        aParentEventDispatcher.registerEventDispatcher(this);
    }

    @Override
    public void dispatch(DispatchableDomainEvent aDispatchableDomainEvent) {
        this.projectWhen(aDispatchableDomainEvent);
    }

    @Override
    public void registerEventDispatcher(EventDispatcher anEventDispatcher) {
        throw new UnsupportedOperationException("Cannot register additional dispatchers.");
    }

    @Override
    public boolean understands(DispatchableDomainEvent aDispatchableDomainEvent) {
        return this.understandsAnyOf(
                aDispatchableDomainEvent.domainEvent().getClass(),
                understoodEventTypes);
    }

    protected void when(CalendarEntryDescriptionChanged anEvent) {
        String sql = "update tbl_vw_calendar_entry set description=:description "
                + " where calendar_entry_id = :calendarEntryId ";
        Map<String, Object> params = new HashMap<>();
        params.put("description", anEvent.description());
        params.put("calendarEntryId", anEvent.calendarEntryId().id());
        this.jdbcTemplate.update(sql, params);
    }

    protected void when(CalendarEntryParticipantInvited anEvent) {
        this.insertInvitee(anEvent.tenant(), anEvent.calendarEntryId(), anEvent.participant());
    }

    protected void when(CalendarEntryParticipantUninvited anEvent) {
        String sql = "delete from tbl_vw_calendar_entry_invitee "
                + "where tenant_id = :tenantId and calendar_entry_id = :calendarEntryId "
                + "and participant_identity = :participantIdentity ";
        Map<String, Object> params = new HashMap<>();
        params.put("tenantId", anEvent.tenant().id());
        params.put("calendarEntryId", anEvent.calendarEntryId().id());
        params.put("participantIdentity", anEvent.participant().identity());
        this.jdbcTemplate.update(sql, params);
    }

    protected void when(CalendarEntryRelocated anEvent) {
        String sql = "update tbl_vw_calendar_entry set location=:location "
                + " where calendar_entry_id = :calendarEntryId ";
        Map<String, Object> params = new HashMap<>();
        params.put("location", anEvent.location());
        params.put("calendarEntryId", anEvent.calendarEntryId().id());
        this.jdbcTemplate.update(sql, params);
    }

    protected void when(CalendarEntryRescheduled anEvent) {
        String sql = "update tbl_vw_calendar_entry "
                + "set alarm_alarm_units = :alarmAlarmUnits, alarm_alarm_units_type = :alarmAlarmUnitsType, "
                + "repetition_ends = :repetitionEnds, repetition_type = :repetitionType, "
                + "time_span_begins = :timeSpanBegins, time_span_ends = :timeSpanEnds "
                + "where tenant_id = :tenantId and calendar_entry_id = :calendarEntryId ";
        Map<String, Object> params = new HashMap<>();
        params.put("alarmAlarmUnits", anEvent.alarm().alarmUnits());
        params.put("alarmAlarmUnitsType", anEvent.alarm().alarmUnitsType().name());
        params.put("repetitionEnds", anEvent.repetition().ends());
        params.put("repetitionType", anEvent.repetition().repeats().name());
        params.put("timeSpanBegins", anEvent.timeSpan().begins());
        params.put("timeSpanEnds", anEvent.timeSpan().ends());
        params.put("tenantId", anEvent.tenant().id());
        params.put("calendarEntryId", anEvent.calendarEntryId().id());
        this.jdbcTemplate.update(sql, params);
    }

    protected void when(CalendarEntryScheduled anEvent) {
        String existSql = "select count(calendar_entry_id) as _count from tbl_vw_calendar_entry "
                + "where tenant_id = :tenantId and calendar_entry_id = :calendarEntryId ";
        Map<String, Object> existParams = new HashMap<>();
        existParams.put("tenantId", anEvent.tenant().id());
        existParams.put("calendarEntryId", anEvent.calendarEntryId().id());
        Integer count = null;
        try {
            count = this.jdbcTemplate.queryForObject(existSql, existParams, Integer.class);
        } catch (EmptyResultDataAccessException e) {
            // ignore
        }
        boolean exist = count != null && count > 0;
        // idempotent operation
        if (exist) {
            return;
        }
        String insertSql = "insert into tbl_vw_calendar_entry( "
                + "calendar_entry_id, alarm_alarm_units, alarm_alarm_units_type, "
                + "calendar_id, description, location, "
                + "owner_email_address, owner_identity, owner_name, "
                + "repetition_ends, repetition_type, "
                + "tenant_id, time_span_begins, time_span_ends"
                + ") values(" +
                ":calendarEntryId,:alarmAlarmUnits,:alarAlarmUnitsType,:calendarId," +
                ":description,:location,:ownerEmailAddress,:ownerIdentity,:ownerName,:repetitionEnds,:repetitionType," +
                ":tenantId,:timeSpanBegins,:timeSpanEnds)";
        Map<String, Object> params = new HashMap<>();
        params.put("calendarEntryId", anEvent.calendarEntryId().id());
        params.put("alarmAlarmUnits", anEvent.alarm().alarmUnits());
        params.put("alarAlarmUnitsType", anEvent.alarm().alarmUnitsType().name());
        params.put("calendarId", anEvent.calendarId().id());
        params.put("description", anEvent.description());
        params.put("location", anEvent.location());
        params.put("ownerEmailAddress", anEvent.owner().emailAddress());
        params.put("ownerIdentity", anEvent.owner().identity());
        params.put("ownerName", anEvent.owner().name());
        params.put("repetitionEnds", anEvent.repetition().ends());
        params.put("repetitionType", anEvent.repetition().repeats().name());
        params.put("tenantId", anEvent.tenant().id());
        params.put("timeSpanBegins", anEvent.timeSpan().begins());
        params.put("timeSpanEnds", anEvent.timeSpan().ends());
        this.jdbcTemplate.update(insertSql, params);

        for (Participant participant : anEvent.invitees()) {
            this.insertInvitee(anEvent.tenant(), anEvent.calendarEntryId(), participant);
        }
    }

    private void insertInvitee(Tenant aTenant,
                               CalendarEntryId aCalendarEntryId,
                               Participant aParticipant) {
        String existSql = "select id from tbl_vw_calendar_entry_invitee "
                + "where tenant_id = :tenantId and calendar_entry_id = :calendarEntryId "
                + "and participant_identity = :participantIdentity ";
        Map<String, Object> existParams = new HashMap<>();
        existParams.put("tenantId", aTenant.id());
        existParams.put("calendarEntryId", aCalendarEntryId.id());
        existParams.put("participantIdentity", aParticipant.identity());
        Integer count = null;
        try {
            count = this.jdbcTemplate.queryForObject(existSql, existParams, Integer.class);
        } catch (EmptyResultDataAccessException e) {
            // ignore
        }
        boolean exist = count != null && count > 0;
        // idempotent operation
        if (exist) {
            return;
        }
        String insertSql = "insert into tbl_vw_calendar_entry_invitee( "
                + "id, calendar_entry_id, "
                + "participant_email_address, participant_identity, participant_name, "
                + "tenant_id"
                + ") values(" +
                ":id,:calendarEntryId,:participantEmailAddress,:participantIdentity," +
                ":participantName,:tenantId)";
        Map<String, Object> params = new HashMap<>();
        params.put("id", 0);
        params.put("calendarEntryId", aCalendarEntryId.id());
        params.put("participantEmailAddress", aParticipant.emailAddress());
        params.put("participantIdentity", aParticipant.identity());
        params.put("participantName", aParticipant.name());
        params.put("tenantId", aTenant.id());
        this.jdbcTemplate.update(insertSql, params);
    }
}
